package com.panopset;

import java.util.StringTokenizer;

/**
 * Convert String representation of a Java method and parameters to a reflection
 * execution.
 *
 * Parameters must be String objects.
 *
 * @author Karl Dinwiddie
 */
public final class ReflectionInvoker {

    /**
     * Map provider.
     */
    private MapProvider pmapProvider;

    /**
     * Object.
     */
    private Object pobject;

    /**
     * Class.
     */
    private Class<?> pclazz;

    /**
     * Method.
     */
    private String pmethod;

    /**
     * Parameters.
     */
    private String pparms;

    /**
     * Use reflection to execute the given method.
     *
     * @return String, empty String if invoked method returns void, otherwise
     *         results of returned object's toString() function.
     * @throws Exception
     *             Exception.
     */
    public String exec() throws Exception {
        StringTokenizer st = new StringTokenizer(pparms, ",");
        Object[] args = new String[st.countTokens()];
        Class<?>[] parmTypes = new Class<?>[st.countTokens()];
        int i = 0;
        while (st.hasMoreTokens()) {
            String key = st.nextToken();
            String val = key;
            if (pmapProvider != null) {
                String s = pmapProvider.get(key);
                if (s != null) {
                    val = s;
                }
            }
            if (val == null) {
                val = key;
            }
            args[i] = val;
            parmTypes[i] = String.class;
            i++;
        }
        Object rtn = pclazz.getMethod(pmethod, parmTypes).invoke(pobject, args);
        if (rtn == null) {
            return "";
        }
        return rtn.toString();
    }

    /**
     *
     * ReflectionInvoker builder.
     *
     */
    public static final class Builder {

        /**
         * Object.
         */
        private Object bobject;

        /**
         * Class name.
         */
        private String bclassName;

        /**
         * Class.
         */
        private Class<?> bclazz;

        /**
         * Method.
         */
        private String bmethod;

        /**
         * Parameters.
         */
        private String bparms;

        /**
         * Map provider.
         */
        private MapProvider bmapProvider;

        /**
         * @return Reflection invoker.
         * @throws Exception
         *             Exception.
         */
        public ReflectionInvoker construct() throws Exception {
            ReflectionInvoker rtn = new ReflectionInvoker();
            rtn.pobject = bobject;
            if (bobject == null) {
                if (bclazz == null) {
                    rtn.pclazz = Class.forName(bclassName);
                } else {
                    rtn.pclazz = bclazz;
                }
            } else {
                rtn.pclazz = bobject.getClass();
            }
            rtn.pmethod = bmethod;
            rtn.pparms = bparms;
            rtn.pmapProvider = bmapProvider;
            return rtn;
        }

        /**
         * Optional, if not specified, either className or classMethodAndParms
         * must be specified.
         *
         * @param object
         *            Object method is to be invoked on.
         * @return Builder.
         */
        public Builder object(final Object object) {
            this.bobject = object;
            return this;
        }

        /**
         * Optional, if not specified, either object or classMethodAndParms must
         * be specified.
         *
         * @param className
         *            className
         * @return Builder.
         */
        public Builder className(final String className) {
            this.bclassName = className;
            return this;
        }

        /**
         * If not specified, static methods only may be invoked.
         *
         * @param clazz
         *            Class.
         * @return Builder.
         */
        public Builder clazz(final Class<?> clazz) {
            this.bclazz = clazz;
            return this;
        }

        /**
         *
         * @param method
         *            methodName
         * @return Builder.
         */
        public Builder method(final String method) {
            this.bmethod = method;
            return this;
        }

        /**
         *
         * @param parms
         *            parm1,parm2
         * @return Builder
         */
        public Builder parms(final String parms) {
            this.bparms = parms;
            return this;
        }

        /**
         * Optional mapProvider. If specified, the mapProvider will be checked
         * for parameter values. If the parameter does not match any of the
         * mapProvider's keys, then the parameter is simply used by itself.
         *
         * @param mapProvider
         *            Map provider.
         * @return Builder.
         */
        public Builder mapProvider(final MapProvider mapProvider) {
            this.bmapProvider = mapProvider;
            return this;
        }

        /**
         *
         * @param methodAndParms
         *            method(parm1,parm2)
         * @return Builder.
         */
        public Builder methodAndParms(final String methodAndParms) {
            int paramsStart = methodAndParms.indexOf("(");
            if (paramsStart < 2) {
                throw new RuntimeException("Format "
                        + "should be function(parms), found:" + methodAndParms);
            }
            bmethod = methodAndParms.substring(0, paramsStart);
            bparms = methodAndParms.substring(paramsStart + 1, methodAndParms
                    .length() - 1);
            return this;
        }

        /**
         * Optional, if not specified either object or className must be
         * specified.
         *
         * @param classMethodAndParms
         *            com.company.Class.method(parms)
         * @return Builder.
         */
        public Builder classMethodAndParms(final String classMethodAndParms) {
            int methodStart = classMethodAndParms.lastIndexOf(".");
            String className = classMethodAndParms.substring(0, methodStart);
            String methodAndParms = classMethodAndParms
                    .substring(methodStart + 1);
            return className(className).methodAndParms(methodAndParms);
        }
    }

    /**
     * Prevent instantiation.
     */
    private ReflectionInvoker() {
    }
}
